<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_InfoCard
 * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Exception.php 2794 2007-01-16 01:29:51Z bkarwin $
 * @author     John Coggeshall <john@zend.com>
 */

/**
 * Zend_InfoCard_Xml_EncryptedData
 */
require_once 'Zend/InfoCard/Xml/EncryptedData.php';

/**
 * Zend_InfoCard_Xml_Assertion
 */
require_once 'Zend/InfoCard/Xml/Assertion.php';

/**
 * Zend_InfoCard_Exception
 */
require_once 'Zend/InfoCard/Exception.php';

/**
 * Zend_InfoCard_Cipher
 */
require_once 'Zend/InfoCard/Cipher.php';

/**
 * Zend_InfoCard_Xml_Security
 */
require_once 'Zend/InfoCard/Xml/Security.php';

/**
 * Zend_InfoCard_Adapter_Interface
 */
require_once 'Zend/InfoCard/Adapter/Interface.php';

/**
 * Zend_InfoCard_Claims
 */
require_once 'Zend/InfoCard/Claims.php';


/**
 * @category   Zend
 * @package    Zend_InfoCard
 * @copyright  Copyright (c) 2005-2007 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @author     John Coggeshall <john@zend.com>
 */
class Zend_InfoCard 
{
	/**
	 * URI for XML Digital Signature SHA1 Digests
	 */
	const DIGEST_SHA1        = 'http://www.w3.org/2000/09/xmldsig#sha1';

	/**
	 * An array of certificate pair files and optional passwords for them to search
	 * when trying to determine which certificate was used to encrypt the transient key
	 *
	 * @var Array
	 */
	protected $_keyPairs;
	
	/**
	 * The instance to use to decrypt public-key encrypted data
	 *
	 * @var Zend_InfoCard_Cipher_Pki_Interface
	 */
	protected $_pkiCipherObj;
	
	/**
	 * The instance to use to decrypt symmetric encrypted data
	 *
	 * @var Zend_InfoCard_Cipher_Symmetric_Interface
	 */
	protected $_symCipherObj;
	
	/**
	 * The InfoCard Adapter to use for callbacks into the application using the component
	 * such as when storing assertions, etc.
	 *
	 * @var Zend_InfoCard_Adapter_Interface
	 */
	protected $_adapter;
	

	/**
	 * InfoCard Constructor
	 *
	 * @throws Zend_InfoCard_Exception
	 */
	public function __construct() 
    {
		$this->_keyPairs = array();
		
		if(!extension_loaded('mcrypt')) {
			throw new Zend_InfoCard_Exception("Use of the Zend_InfoCard component requires the mcrypt extension to be enabled in PHP");
		}
		
		if(!extension_loaded('openssl')) {
			throw new Zend_InfoCard_Exception("Use of the Zend_InfoCard component requires the openssl extension to be enabled in PHP");
		}
	}

	/**
	 * Sets the adapter uesd for callbacks into the application using the component, used
	 * when doing things such as storing / retrieving assertions, etc.
	 *
	 * @param Zend_InfoCard_Adapter_Interface $a The Adapter instance
	 * @return Zend_InfoCard The instnace
	 */
	public function setAdapter(Zend_InfoCard_Adapter_Interface $a) 
    {
		$this->_adapter = $a;
		return $this;
	}
	
	/**
	 * Retrieves the adapter used for callbacks into the application using the component.
	 * If no adapter was set then an instance of Zend_InfoCard_Adapter_Default is used
	 *
	 * @return Zend_InfoCard_Adapter_Interface The Adapter instance
	 */
	public function getAdapter() 
    {
		if(is_null($this->_adapter)) {
			Zend_Loader::loadClass('Zend_InfoCard_Adapter_Default');
			$this->setAdapter(new Zend_InfoCard_Adapter_Default());
		}
		
		return $this->_adapter;
	}
	
	/**
	 * Gets the Public Key Cipher object used in this instance
	 *
	 * @return Zend_InfoCard_Cipher_Pki_Interface
	 */
	public function getPkiCipherObject() 
    {
		return $this->_pkiCipherObj;
	}
	
	/**
	 * Sets the Public Key Cipher Object used in this instance
	 *
	 * @param Zend_InfoCard_Cipher_Pki_Interface $cipherObj
	 * @return Zend_InfoCard
	 */
	public function setPkiCipherObject(Zend_InfoCard_Cipher_Pki_Interface $cipherObj) 
    {
		$this->_pkiCipherObj = $cipherObj;	
		return $this;
	}
	
	/**
	 * Get the Symmetric Cipher Object used in this instance
	 *
	 * @return Zend_InfoCard_Cipher_Symmetric_Interface
	 */
	public function getSymCipherObject() 
    {
		return $this->_symCipherObj;
	}
	
	/**
	 * Sets the Symmetric Cipher Object used in this instance
	 *
	 * @param Zend_InfoCard_Cipher_Symmetric_Interface $cipherObj
	 * @return Zend_InfoCard
	 */
	public function setSymCipherObject($cipherObj) 
    {
		$this->_symCipherObj = $cipherObj;
		return $this;
	}
		
	/**
	 * Remove a Certificate Pair by Key ID from the search list
	 *
	 * @throws Zend_InfoCard_Exception
	 * @param string $key_id The Certificate Key ID returned from adding the certificate pair 
	 * @return Zend_InfoCard
	 */
	public function removeCertificatePair($key_id) 
    {
		
		if(!key_exists($key_id, $this->_keyPairs)) {
			throw new Zend_InfoCard_Exception("Attempted to remove unknown key id: $key_id");
		}
		
		unset($this->_keyPairs[$key_id]);
		return $this;
	}
	
	/**
	 * Add a Certificate Pair to the list of certificates searched by the component
	 *
	 * @throws Zend_InfoCard_Exception
	 * @param string $private_key_file The path to the private key file for the pair
	 * @param string $public_key_file The path to the certificate / public key for the pair
	 * @param string $type (optional) The URI for the type of key pair this is (default RSA with OAEP padding)
	 * @param string $password (optional) The password for the private key file if necessary
	 * @return string A key ID representing this key pair in the component
	 */
	public function addCertificatePair($private_key_file, $public_key_file, $type = Zend_InfoCard_Cipher::ENC_RSA_OAEP_MGF1P, $password = null) 
    {
		if(!file_exists($private_key_file) ||
		   !file_exists($public_key_file)) {
		   	throw new Zend_InfoCard_Exception("Could not locate the public and private certificate pair files: $private_key_file, $public_key_file");
		} 
		
		if(!is_readable($private_key_file) || 
		   !is_readable($public_key_file)) {
		   	throw new Zend_InfoCard_Exception("Could not read the public and private certificate pair files (check permissions): $private_key_file, $public_key_file");
  	    }
  	    
  	    $key_id = md5($private_key_file.$public_key_file);
  	    
  	    if(key_exists($key_id, $this->_keyPairs)) {
  	    	throw new Zend_InfoCard_Exception("Attempted to add previously existing certificate pair: $private_key_file, $public_key_file");
  	    }
  	    
  	    switch($type) {
  	    	case Zend_InfoCard_Cipher::ENC_RSA:
  	    	case Zend_InfoCard_Cipher::ENC_RSA_OAEP_MGF1P:
		  	    $this->_keyPairs[$key_id] = array('private' => $private_key_file,
                                  'public'      => $public_key_file,
                                  'type_uri'    => $type);
                                  
                if(!is_null($password)) {
                	$this->_keyPairs[$key_id]['password'] = $password;
                } else {
                	$this->_keyPairs[$key_id]['password'] = null;
                }
                
                return $key_id;
  	    		break;
  	    	default:
  	    		throw new Zend_InfoCard_Exception("Invalid Certificate Pair Type specified: $type");
  	    }
	}
	
	/**
	 * Return a Certificate Pair from a key ID
	 *
	 * @throws Zend_InfoCard_Exception
	 * @param string $key_id The Key ID of the certificate pair in the component
	 * @return array An array containing the path to the private/public key files, 
	 *               the type URI and the password if provided
	 */
	public function getCertificatePair($key_id) 
    {
		if(key_exists($key_id, $this->_keyPairs)) {
			return $this->_keyPairs[$key_id];
		} 
		
		throw new Zend_InfoCard_Exception("Invalid Certificate Pair ID provided: $key_id");
	}
	
	/**
	 * Retrieve the digest of a given public key / certificate using the provided digest
	 * method
	 *
	 * @throws Zend_InfoCard_Exception 
	 * @param string $key_id The certificate key id in the component
	 * @param string $digestMethod The URI of the digest method to use (default SHA1)
	 * @return string The digest value in binary format 
	 */
	protected function _getPublicKeyDigest($key_id, $digestMethod = self::DIGEST_SHA1) 
    {
		$certificatePair = $this->getCertificatePair($key_id);		
		
		$temp = file($certificatePair['public']);
		unset($temp[count($temp)-1]);
		unset($temp[0]);
		$certificateData = base64_decode(implode("\n", $temp));
		
		switch($digestMethod) {
			case self::DIGEST_SHA1:
				$digest_retval = sha1($certificateData, true);
				break;
			default:
				throw new Zend_InfoCard_Exception("Invalid Digest Type Provided: $digestMethod");
		}
		
		return $digest_retval;
	}
	
	/**
	 * Find a certificate pair based on a digest of its public key / certificate file
	 *
	 * @param string $digest The digest value of the public key wanted in binary form
	 * @param string $digestMethod The URI of the digest method used to calculate the digest
	 * @return mixed The Key ID of the matching certificate pair or false if not found
	 */
	protected function _findCertifiatePairByDigest($digest, $digestMethod = self::DIGEST_SHA1) 
    {
		
		foreach($this->_keyPairs as $key_id => $certificate_data) {

			$cert_digest = $this->_getPublicKeyDigest($key_id, $digestMethod);
		
			if($cert_digest == $digest) {
				return $key_id;
			}
		}
		
		return false;
	}
	
	/**
	 * Extracts the Signed Token from an EncryptedData block
	 *
	 * @throws Zend_InfoCard_Exception
	 * @param string $strXmlToken The EncryptedData XML block
	 * @return string The XML of the Signed Token inside of the EncryptedData block
	 */
	protected function _extractSignedToken($strXmlToken) 
    {
		$encryptedData = Zend_InfoCard_Xml_EncryptedData::getInstance($strXmlToken);
		
		// Determine the Encryption Method used to encrypt the token
		
		switch($encryptedData->getEncryptionMethod()) {
			case Zend_InfoCard_Cipher::ENC_AES128CBC:
			case Zend_InfoCard_Cipher::ENC_AES256CBC:
				break;
			default:
				throw new Zend_InfoCard_Exception("Unknown Encryption Method used in the secure token");
		}
		
		// Figure out the Key we are using to decrypt the token
		
		$keyinfo = $encryptedData->getKeyInfo();
		
		if(!($keyinfo instanceof Zend_InfoCard_Xml_KeyInfo_XmlDSig)) {
			throw new Zend_InfoCard_Exception("Expected a XML digital signature KeyInfo, but was not found");
		}
		
		
		$encryptedKey = $keyinfo->getEncryptedKey();
		
		switch($encryptedKey->getEncryptionMethod()) {
			case Zend_InfoCard_Cipher::ENC_RSA:
			case Zend_InfoCard_Cipher::ENC_RSA_OAEP_MGF1P:
				break;
			default:
				throw new Zend_InfoCard_Exception("Unknown Key Encryption Method used in secure token");
		}
		
		$securityTokenRef = $encryptedKey->getKeyInfo()->getSecurityTokenReference();
		
		$key_id = $this->_findCertifiatePairByDigest($securityTokenRef->getKeyReference());
		
		if(!$key_id) {
			throw new Zend_InfoCard_Exception("Unable to find key pair used to encrypt symmetric InfoCard Key");
		}
		
		$certificate_pair = $this->getCertificatePair($key_id);
		
		// Santity Check
		
		if($certificate_pair['type_uri'] != $encryptedKey->getEncryptionMethod()) {
			throw new Zend_InfoCard_Exception("Certificate Pair which matches digest is not of same algorithm type as document, check addCertificate()");
		}
		
		$PKcipher = Zend_InfoCard_Cipher::getInstanceByURI($encryptedKey->getEncryptionMethod());
		
		$symmetricKey = $PKcipher->decrypt(base64_decode($encryptedKey->getCipherValue(), true), file_get_contents($certificate_pair['private']), $certificate_pair['password']);
		
		$symCipher = Zend_InfoCard_Cipher::getInstanceByURI($encryptedData->getEncryptionMethod());
		
		$signedToken = $symCipher->decrypt(base64_decode($encryptedData->getCipherValue(), true), $symmetricKey);

		return $signedToken;		
	}
	
	/**
	 * Process an input Infomation Card EncryptedData block sent from the client, 
	 * validate it, and return the claims contained within it on success or an error message on error
	 *
	 * @param string $strXmlToken The XML token sent to the server from the client
	 * @return Zend_Infocard_Claims The Claims object containing the claims, or any errors which occurred
	 */
	public function process($strXmlToken) 
    {
		
		$retval = new Zend_InfoCard_Claims();
		
		try {
			$signedAssertionsXml = $this->_extractSignedToken($strXmlToken);
		} catch(Zend_InfoCard_Exception $e) {
			$retval->setError('Failed to extract assertion document');
			$retval->setCode(Zend_InfoCard_Claims::RESULT_PROCESSING_FAILURE);
			return $retval;
		}
		
		try {
			$assertions = Zend_InfoCard_Xml_Assertion::getInstance($signedAssertionsXml);
		} catch(Zend_InfoCard_Exception $e) {
			$retval->setError('Failure processing assertion document');
			$retval->setCode(Zend_InfoCard_Claims::RESULT_PROCESSING_FAILURE);
			return $retval;
		}
		
		if(!($assertions instanceof Zend_InfoCard_Xml_Assertion_Interface)) {
			throw new Zend_InfoCard_Exception("Invalid Assertion Object returned");
		}
		
		if(!($reference_id = Zend_InfoCard_Xml_Security::validateXMLSignature($assertions->asXML()))) {
			$retval->setError("Failure Validating the Signature of the assertion document");
			$retval->setCode(Zend_InfoCard_Claims::RESULT_VALIDATION_FAILURE);
			return $retval;
		}

		// The reference id should be locally scoped as far as I know
		if($reference_id[0] == '#') {
			$reference_id = substr($reference_id, 1);
		} else {
			$retval->setError("Reference of document signature does not reference the local document");
			$retval->setCode(Zend_InfoCard_Claims::RESULT_VALIDATION_FAILURE);
			return $retval;
		}
		
		// Make sure the signature is in reference to the same document as the assertions
		if($reference_id != $assertions->getAssertionID()) {
			$retval->setError("Reference of document signature does not reference the local document");
			$retval->setCode(Zend_InfoCard_Claims::RESULT_VALIDATION_FAILURE);
		}
		
		// Validate we haven't seen this before and the conditions are acceptable
		$conditions = $this->getAdapter()->retrieveAssertion($assertions->getAssertionURI(), $assertions->getAssertionID());

		if($conditions === false) {
			$conditions = $assertions->getConditions();
		}
		
		
		if(is_array($condition_error = $assertions->validateConditions($conditions))) {
			$retval->setError("Conditions of assertion document are not met: {$condition_error[1]} ({$condition_error[0]})");
			$retval->setCode(Zend_InfoCard_Claims::RESULT_VALIDATION_FAILURE);
		}
					
		$attributes = $assertions->getAttributes();
		
		$retval->setClaims($attributes);
		
		if($retval->getCode() == 0) {
			$retval->setCode(Zend_InfoCard_Claims::RESULT_SUCCESS);
		}
		
		return $retval;
	}
}
